Web scraping
La posibilidad de utilizar la información que está vertida en internet sin lugar a dudas abre una oportunidad inédita para diversos análisis que queramos hacer. R tiene un conjunto de funciones que nos permiten realizar esta tarea de una manera relativamente simple. En esta nota de clase vamos a tener una introducción a cómo poder obtener esta información en un dataset listo para tarbajar.
Cargando los paquetes
Para esta clase vamos a usar algunos paquetes bien clásicos como tidyverse, pero también vamos a introducirnos de lleno a rvest. Para lo que sigue, tenemos que tener cargadas estas librerías así que aprovechamos para hacerlo ahora. Recuerden que deben tener instalados estos paquetes, pueden hacerlo con la función install.packages()
Ahora sí, ya podemos empezar a pensar cómo scrapear una página web. El primer paso es comprender cómo se estructuran. Hacía ahí vamos.
Estructura de una página web
Cualquier página web, no importa qué tan compleja o como interactúe con el cliente, quien pide la información, y el servidor, donde se encuentran esos datos, tiene una estructura en cómun: Hyper Text Markup Language, más conocido como HTML. Para hacerlo fácil, HTML es un conjunto de etiquetas que definen los objetos (y sus atributos) que en última instancia especifícan cómo se estructura la información en las páginas web que navegamos.
¿Por qué es importante entender esto? Por la sencilla razón que tenemos que conocer este “lenguaje” para poder comprender dónde se encuentra esa porción de la información que queremos almacenar. Empecemos por lo básico: abran Google Chrome - vamos a usar este navegador para las dos clases de scraping - y entren a la siguiente página de wikipedia: https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel
Una vez que lo haya cargado, vayan a los tres puntos verticales que tienen en la esquina superior derecha -> Más herramientas -> Herramientas del desarrollador (obviamente todo esto puede cambiar de nombre si lo tienen instalado en inglés). Pista: Pueden usar el atajo Ctrl+Shift+I de ahora en adelante. Van a encontrarse con un panel con muchas opciones… de a poco vamos a ir viendo qué nos pueden aportar cada una de ellas. Pueden usar el gif para guiarse
Cómo acceder a las herramientas de desarrollador en Google Chrome
Si colapsan <head> y <body> se van a encontrar con tres grandes “llaves”: <html>, <head> y <body>. Cada una de estas etiquetas definen el inicio de un objeto. En el caso de <html> indica el comienzo de una página, <head> contiene información muy importante como scripts de javascript o estilos de CSS, que prácticamente cuando estamos haciendo scraping no es necesario de investigar. Donde sucede toda la magia en nuestra tarea de scrapear es en <body> y todos los elementos que aparecen adentro de ella. Un detalle importante: cuando se introduce una etiqueta con una barra al principio, como en </body> indica que ese objeto termina en ese lugar.
El explorador de elementos de Google Chrome es muy inteligente. Prueben ir seleccionando los <div>, que se encuentran dentro de head, y van a ver que en la página que están navegando les va a aparecer coloreado en azul a que parte de la página estamos hace referencia. A la etiqueta <div> nos la vamos a encontrar en muchas situaciones, ya que funciona como “contenedor” de otro conjunto de objetos.
No es el objetivo de esta clase hacer un repaso exhaustivo de cada una de las etiquetas que existen en HTML, pero en https://www.w3schools.com/html/html_intro.asp tienen mucha información sobre cada una de ellas. Vayamos a lo importante: encontrar dónde está lo que queremos scrapear.
Identificando el elemento que contiene la información
¿Cómo podemos identificar exactamente en qué elemento está la información que queremos extraer? De nuevo una ayuda invaluable está en en simple navegador de Google Chrome. Imaginemos que queremos extraer el nombre del titulo de la página, eso que vemos en Anexo: Ganadores del Premio Nobel. Para extraerlo, tenemos que decirle a R dónde está esa información.
Hagan click secundario (o derecho) encima del título y elijan inspeccionar. Si no tenían abiertas las herramientas de desarrollador, se los va a abrir. Lo que va a hacer es posicionarnos exactamente en el elemento que contiene este texto. En este caso podemos ver el elemento es el siguiente:
<h1 id=“firstHeading” class=“firstHeading” lang=“es”>Anexo:Ganadores del Premio Nobel</h1>
Cómo identificar la etiqueta que contiene la información que queremos scrapear
El texto es lo que está entre la etiqueta <h1> y </h1>, que lo que indica son Headers, siendo h1 típicamente el de mayor importancia y tamaño. Importante: vean que dentro de la primera etiqueta nos indica también distintos atributos como id, class o lang. Los atributos son específicos a cada uno de estos elementos. id suele ser muy importante, ya que podemos identificar por nombre al elemento, mientras que la class hace referencia a la clase CSS, que es la que le da un estilo (por ejemplo, cierta tipografía, tamaño, etc, etc). lang es un atributo más extraño y es específico a la forma en la que desarrollaron la página de wikipedia.
Usando rvest para extraer información de las páginas
Ahora que ya identificamos donde está el titulo, veamos cómo le decimos a rvest que queremos el texto que se encuentra dentro de esa etiqueta:
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = "h1") %>%
html_text()## [1] "Anexo:Ganadores del Premio Nobel"
Funcionó, pero vamos a explicar bien qué es lo que hace cada parte
- read_html(x = “https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel”): Se encarga de leer el html de cualquier página web. Veamos qué es lo que devuelve
## {html_document}
## <html class="client-nojs" lang="es" dir="ltr">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n<meta charset= ...
## [2] <body class="mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-104 ns-subject mw-editable page ...
Les resulta familiar? Debería ! es la estructura de la página HTML. Lo que hace read_html() es descargar la página y convertirla a un documento XML, otra forma de representar estructura de datos con etiquetas. No es necesario que sepamos manejar documentos XML, ya que rvest nos permite seleccionar a diferentes elementos de la página mediante CSS selectors, lo que nos lleva a la segunda parte de nuestro pequeño código
- html_node(css = “h1”)
La función html_node() nos devuelve los nodos (etiquetas) que seleccionamos según dos formas de elegirlos: CSS selector o xpath. En esta clase vamos a ver CSS selectors, o al menos sus usos más comunes. Veamos: si escribimos h1 - o cualquier otro nombre de etiqueta - html_node() nos devolverá la primera vez que aparece esa etiqueta y html_nodes() todos los nodos con esa etiqueta. Ejecutemos solo esa parte
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = "h1")## {html_node}
## <h1 id="firstHeading" class="firstHeading" lang="es">
Como podemos ver, nos devolvió un nodo, pero sin indicar el texto que está dentro de se nodo. Ahora bien, usando # podemos encontrar el mismo nodo, pero mediante el valor que tiene el atributo id
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = "#firstHeading")## {html_node}
## <h1 id="firstHeading" class="firstHeading" lang="es">
Esto puede ser muy útil, porque muchas veces tenemos muchos objetos, pero solo uno se llama de esta manera. Sin embargo, no todas las páginas lo hacen tan fácil, y muchas veces tenemos que buscar por la clase, u otros atributos. Para la clase también hay un caracter reservado que es el . (un punto), presten atención al siguiente código:
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = ".firstHeading")## {html_node}
## <h1 id="firstHeading" class="firstHeading" lang="es">
Devolvió el mismo elemento. Podemos generalizar todavía más esta forma de encontrar a las etiquetas por sus atributos de la siguiente manera
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = "h1[class=firstHeading]")## {html_node}
## <h1 id="firstHeading" class="firstHeading" lang="es">
read_html(x = "https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>%
html_node(css = "h1[id=firstHeading]")## {html_node}
## <h1 id="firstHeading" class="firstHeading" lang="es">
También estamos seleccionando exactamente al mismo elemento. La lógica es poner al elmento y luego “filtrar” según algún atributo que hayamos visto que tenía asociado y su valor. De esta manera podemos usar cualquier atributo que cualquiera de las páginas de internet use.
Nos falta la tercera línea de nuestro código:
- html_text()
Esta función de rvest lo único que hace es extraer el texto que se encuentra dentro del nodo o nodos que ya hayamos elegido anteriormente. No es lo único que podemos extraer de ellos, incluso podemos extraer valores de atributos o directamente la tabla como un data frame, como ya veremos en ejemplos más avanzados.
Su turno: extrayendo los idiomas en los que está disponible la página
Como ya deben todos saber, Wikipedia ofrece sus entradas sobre diversos temas en muchos idiomas ¿Pueden identificar donde aparece esta información? En el lado izquierdo de la página. Encuentren un patrón en común entre estos links como para poder seleccionarlos y extraer su texto. Importante: van a tener que usarl html_nodes() en lugar de html_node(), ya que estamos queriendo seleccionar más de un nodo.
.
.
.
.
.
.
.
.
.
.
.
.
.
Una forma de resolverlo es la siguiente:
Extrayendo la tabla de personas que ganaron el premio Nobel
El objetivo de esta clase va a ser lograr armar un dataset de información sobre ganadores y ganadoras de premios nóbeles. Para eso, nuestro primer objetivo va a ser tener a todos los galardonados con este premio, para luego poder entrar a cada una de sus perspectivas páginas y obtener información.
Extraer la tabla de los ganadores puede ser más fácil de lo que parece. Primero tenemos que hacer lo de siempre: identificar en qué etiqueta está ubicado, en el siguiente gif replicamos lo que ya hicimos anteriormente e identificamos que se encuentra en una etiqueta <table> con una clase que parece bastante normal para todas las tablas de wikipedia.
Identificando dónde está la tabla de ganadores del Nobel
tablaNobel <- read_html("https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel") %>% html_node(css ="table") %>%
html_table()
head(tablaNobel)## Año Física
## 1 1901 Röntgen, Wilhelm ConradWilhelm Conrad Röntgen
## 2 1902 Lorentz, Hendrik A.Hendrik A. Lorentz;Zeeman, PieterPieter Zeeman
## 3 1903 Becquerel, HenriHenri Becquerel;Curie, PierrePierre Curie;Curie, MarieMarie Curie
## 4 1904 John William Strutt
## 5 1905 Philipp Lenard
## 6 1906 Thomson, J. J.J. J. Thomson
## Química
## 1 Hoff, Jacobus H. van 'tJacobus H. van 't Hoff
## 2 Fischer, Hermann EmilHermann Emil Fischer
## 3 Arrhenius, SvanteSvante Arrhenius
## 4 William Ramsay
## 5 Adolf von Baeyer
## 6 Moissan, HenriHenri Moissan
## Fisiologíao Medicina
## 1 Behring, Emil vonEmil von Behring
## 2 Ross, RonaldRonald Ross
## 3 Finsen, Niels RybergNiels Ryberg Finsen
## 4 Ivan Pavlov
## 5 Robert Koch
## 6 Golgi, CamilloCamillo Golgi;Ramón y Cajal, SantiagoSantiago Ramón y Cajal
## Literatura
## 1 Prudhomme, SullySully Prudhomme
## 2 Mommsen, TheodorTheodor Mommsen
## 3 Bjørnson, BjørnstjerneBjørnstjerne Bjørnson
## 4 Frédéric Mistral;\nJosé Echegaray
## 5 Henryk Sienkiewicz
## 6 Carducci, GiosuèGiosuè Carducci
## Paz
## 1 Dunant, HenryHenry Dunant;Passy, FrédéricFrédéric Passy
## 2 Ducommun, ÉlieÉlie Ducommun;Gobat, AlbertAlbert Gobat
## 3 Cremer, William RandalWilliam Randal Cremer
## 4 Instituto de Derecho Internacional
## 5 Bertha von Suttner
## 6 Roosevelt, TheodoreTheodore Roosevelt
## Premio en Ciencias Económicas en memoria de Alfred Nobel
## 1 —
## 2 —
## 3 —
## 4 —
## 5 —
## 6 —
Mmm… parece que no era tan fácil después de todo ! Si usan View() van a poder ver más en claro qué es lo que pasó. Por algúna razón, está duplicando el nombre de las personas que ganaron el nobel ¿Puede haber sido un problema de rvest? Probablemente no. Inspeccionemos un poco más la estructura de la tabla.
A grandes rasgos, una tabla en HTML está compuesta por filas (<tr>) y “elementos” dentro de cada una de estas filas, que sumados conformaran las columnas (<td>). Si se fijan dentro de cada una de las entradas, van a ver que hay un elemento que dice <span style=“display:none;”> y contiene el nombre de la persona. Además, existe otro <span class=“fn”> que tiene dentro de él un objeto <a>, donde hay información sobre el link de wikipedia de esa persona y además contiene el nombre también como texto ! Es por esta razón que tenemos este problema con la tabla: aunque nosotros la vemos bien en nuestro navegador, en la estructura de la página está “escondido” esa etiqueta span con display:none. Tenemos que eliminarlo para que html_table() funcione bien.
Para eliminar nodos - partes de la estructura de la página - podemos usar la función xml_remove(), que funciona de una manera distinta a la que estamos acostumbrados en R. No es necesario asignar ese resultado a nada: automáticamente toma el último documento XML que hayamos trabajado - en este caso, el objeto que voy a crear, pagina - y elimina los nodos que hayamos seleccionado. Luego, ya podemos usar directamente extraer la tabla
pagina <- read_html("https://es.wikipedia.org/wiki/Anexo:Ganadores_del_Premio_Nobel")
nodosEliminar <- pagina %>% html_nodes("span[style='display:none;']")
xml_remove(nodosEliminar)
tabla <- pagina %>%
html_node(css ="table") %>%
html_table()
glimpse(tabla)## Rows: 120
## Columns: 7
## $ Año [3m[38;5;246m<chr>[39m[23m "1901", "1902", "1903", "1...
## $ Física [3m[38;5;246m<chr>[39m[23m "Wilhelm Conrad Röntgen", ...
## $ Química [3m[38;5;246m<chr>[39m[23m "Jacobus H. van 't Hoff",...
## $ `Fisiologíao Medicina` [3m[38;5;246m<chr>[39m[23m "Emil von Behring", "Ronal...
## $ Literatura [3m[38;5;246m<chr>[39m[23m "Sully Prudhomme", "Theodo...
## $ Paz [3m[38;5;246m<chr>[39m[23m "Henry Dunant;Frédéric Pas...
## $ `Premio en Ciencias Económicas en memoria de Alfred Nobel` [3m[38;5;246m<chr>[39m[23m "—", "—", "—", "—", "—", "...
Todo anda bien ahora.. Pero a esta tabla tenemos que hacerle mucha transformación de datos para que nos sirva. Por el momento, vamos a hacer una última parte en esta secuencia: extraer los links que nos van a servir para completar información relevante.
links<- pagina %>%
html_nodes("td a") %>%
html_attr("href")
nombres<- pagina %>%
html_nodes("td a") %>%
html_text() %>%
trimws()
linksNombre <- tibble(nombres,links)
linksNombre <- unique(linksNombre)Varias cosas sucedieron acá. Para empezar, usamos una herramienta más de los CSS Selectors: combinamos etiquetas. Lo que hace es pedir que la etiqueta a la derecha sea hija (este contenida) por la etiqueta de la izquierda. Si prestan atención en la estructura de la página, los links están dentro de <td>. Por otro lado, usamos html_attr(), en lugar de html_text() o html_table() como veníamos trabajando hasta el momento. Lo que hace esa función es devolvernos el valor que tiene un atributo que le pedimos, en este caso href que tiene parte del hipervínculo donde existe más información sobre cada uno de los premiados.
Data wrangling
Scrapear siempre requiere una buena dosis de transformación de datos para hacer útiles los datos con los que estamos trabajando. El objetivo final va a ser una tabla donde tengamos información sobre el género, el año de nacimiento, si están fallecidos o no y cualquier otra información que podamos juntar como para poder realizar algún análisis.
El primer paso para esto es separar a los Nobels que aparecen juntos en un mismo año. Están separados por “;” para cada una de las disciplinas. Lo que nos conviene hacer, primero, es pasar de formato ancho a largo este data frame, con el objetivo de poder separar a todos los ganadores más fácilmente:
tabla <- tabla %>%
slice(-nrow(tabla)) %>%
pivot_longer(cols = -1,names_to="Disciplina",values_to="Ganadores")Ahora también deberíamos eliminar los casos para los cuales no hubo ganadores. Si miran la tabla van a ver que siempre que no se entregó el premio va a aparecer el texto **No se entregó* o —, particularmente para el caso de los premios para Economía. Vamos a filtrar esos casos:
Bien, ahora tenemos que separar según los “;” y ya (casi) estamos
## Warning: Expected 3 pieces. Missing pieces filled with `NA` in 491 rows [1, 2, 3, 4, 5, 6, 7, 8,
## 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, ...].
## Rows: 597
## Columns: 5
## $ Año [3m[38;5;246m<chr>[39m[23m "1901", "1901", "1901", "1901", "1901", "1902", "1902", "1902", "1902", "1...
## $ Disciplina [3m[38;5;246m<chr>[39m[23m "Física", "Química", "Fisiologíao Medicina", "Literatura", "Paz", "Física"...
## $ Ganador1 [3m[38;5;246m<chr>[39m[23m "Wilhelm Conrad Röntgen", "Jacobus H. van 't Hoff", "Emil von Behring", "...
## $ Ganador2 [3m[38;5;246m<chr>[39m[23m NA, NA, NA, NA, "Frédéric Passy", "Pieter Zeeman", NA, NA, NA, "Albert Gob...
## $ Ganador3 [3m[38;5;246m<chr>[39m[23m NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "Marie Curie", NA, NA, NA, NA, NA,...
tabla <- pivot_longer(tabla,cols = -c(1,2),names_to = "NroGanador",values_to="Ganador")
glimpse(tabla)## Rows: 1,791
## Columns: 4
## $ Año [3m[38;5;246m<chr>[39m[23m "1901", "1901", "1901", "1901", "1901", "1901", "1901", "1901", "1901", "1...
## $ Disciplina [3m[38;5;246m<chr>[39m[23m "Física", "Física", "Física", "Química", "Química", "Química", "Fisiología...
## $ NroGanador [3m[38;5;246m<chr>[39m[23m "Ganador1", "Ganador2", "Ganador3", "Ganador1", "Ganador2", "Ganador3", "G...
## $ Ganador [3m[38;5;246m<chr>[39m[23m "Wilhelm Conrad Röntgen", NA, NA, "Jacobus H. van 't Hoff", NA, NA, "Emil...
Perfecto, ahora solo tenemos que eliminar a los que son NAs - y también vamos a perder a la columna NroGanador, ya que no nos interesa el orden en que son nombrados
tabla <- tabla %>%
filter(!is.na(Ganador)) %>%
mutate(Ganador=trimws(Ganador)) %>%
select(-NroGanador)Finalmente, unimos los links según el nombre para poder agregar más información en nuestro próximo paso
tabla<- tabla %>%
mutate(Ganador=trimws(Ganador))
tabla <- left_join(tabla,linksNombre,by=c("Ganador"="nombres"))
sum(is.na(tabla$links))## [1] 2
Funcionó… pero nos quedaron dos ganadores sin link, veamos qué tienen en común:
## [1] "Tenzin Gyatso, el XIV Dalái Lama" "Olga Tokarczuk\notorgado en el año 2019"
En ambos casos el problema es que el nombre no coincide con la descripción que aparece en la tabla de ganadores. En el primer caso, se soluciona con eliminar la parte de “el XIV Dalái Lama”. En el segundo caso es todavía más fácil, porque se trata de un caso en el cual lo anunciaron en 2018, pero se lo dieron en 2019, con lo cual simplemente hay que filtrar este caso ya que estaríamos doble contando
tabla <- tabla %>%
mutate(Ganador=gsub(", el XIV Dalái Lama","",Ganador)) %>%
filter(Ganador != "Olga Tokarczuk\notorgado en el año 2019") %>%
select(-links)
tabla <- tabla %>%
left_join(linksNombre, by=c("Ganador"="nombres"))
sum(is.na(tabla %>% pull(links)))## [1] 0
Analizando nuestros datos
Ahora que scrapeamos nuestros datos, analicemos algunos patrones. Para empezar ¿Cómo evolucionó la cantidad de galardones por rama y año desde que comenzaron los premios Nobel?
resumenCantidad <- tabla %>%
mutate(decada=cut(as.numeric(Año),breaks=seq(1900,2020,by=10), include.lowest=TRUE,labels=paste(seq(1900,2010,by=10),"s",sep=""))) %>%
group_by(Año,Disciplina) %>%
mutate(cant=n()) %>%
group_by(decada,Disciplina) %>%
summarise(prop1=sum(cant %in% 1)/n())
library(ggthemes)
ggplot(resumenCantidad) +
geom_line(aes(x=decada,y=prop1,color=Disciplina,group=Disciplina),size=1.5) +
theme_fivethirtyeight() +
scale_color_wsj(name="") +
scale_y_continuous(labels = scales::percent_format(1)) +
labs(title="Premios nobel",subtitle="Proporción de galardones entregados a una sola persona por década y disciplina",caption="Elaboración propia con base en datos de Wikipedia")Como pueden ver, con excepción quizás del premio nóbel de la paz y de literatura, los premios a la Física, Fisiología/Medicina y a la Química han pasado de entregarse a una sola persona durante la primera parte del SXX a ser una rareza desde entonces.
Hagamos una segunda pregunta ¿Cómo se distribuye la proporción de mujeres entre el total de personas que recibieron el premio, por década?
resumenGenero <- tabla %>%
filter(is.na(resultado)) %>%
mutate(decada=cut(as.numeric(Año),breaks=seq(1900,2020,by=10), include.lowest=TRUE,labels=paste(seq(1900,2010,by=10),"s",sep=""))) %>%
group_by(decada,Disciplina) %>%
summarise(propMujeres=sum(genero %in% "Mujeres")/n())
ggplot(resumenGenero) +
geom_line(aes(x=decada,y=propMujeres,color=Disciplina,group=Disciplina),size=1.5) +
theme_fivethirtyeight() +
scale_color_wsj(name="") +
scale_y_continuous(labels = scales::percent_format(1),limits = c(0,1)) +
labs(title="Premios nobel",subtitle="Proporción de galardones entregados a una mujer por década y disciplina",caption="Elaboración propia con base en datos de Wikipedia")Ejercicios
Medio: Hacer gráficos con ggplot para observar que pasó con la edad promedio de los ganadores de Premios Nobel
Difícil: Limpiar las nacionalidades y hacer un ranking por ellas. Tips: pueden usar separate() y pivot_longer() para separar a las nacionalidades multiples. También pueden usar tolower()